<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>K线图组件</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      background-color: #1e1e2e;
      color: #e0e0e0;
      padding: 20px;
    }
    .container {
      max-width: 1200px;
      margin: 0 auto;
    }
    .header {
      text-align: center;
      margin-bottom: 20px;
    }
    .header h1 {
      color: #6c5ce7;
      margin-bottom: 10px;
    }
    .chart-container {
      background-color: #2d2d44;
      border-radius: 10px;
      padding: 20px;
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
      margin-bottom: 20px;
    }
    .chart-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 15px;
    }
    .stock-info {
      display: flex;
      gap: 20px;
    }
    .price {
      font-size: 1.2em;
      font-weight: bold;
    }
    .price.up {
      color: #00b894;
    }
    .price.down {
      color: #ff7675;
    }
    .controls {
      display: flex;
      gap: 10px;
    }
    button {
      background-color: #6c5ce7;
      color: white;
      border: none;
      padding: 8px 15px;
      border-radius: 5px;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    button:hover {
      background-color: #5b4bc4;
    }
    .chart-wrapper {
      position: relative;
      width: 100%;
      height: 500px;
      border: 1px solid #444;
      border-radius: 5px;
      overflow: hidden;
    }
    canvas {
      display: block;
      background-color: #1e1e2e;
    }
    .legend {
      display: flex;
      gap: 15px;
      margin-top: 10px;
      font-size: 0.9em;
    }
    .legend-item {
      display: flex;
      align-items: center;
      gap: 5px;
    }
    .legend-color {
      width: 15px;
      height: 3px;
    }
    .tooltip {
      position: absolute;
      background-color: rgba(45, 45, 68, 0.9);
      border: 1px solid #555;
      border-radius: 5px;
      padding: 10px;
      pointer-events: none;
      z-index: 100;
      font-size: 0.9em;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
    }
    .tooltip-line {
      display: flex;
      justify-content: space-between;
      margin-bottom: 5px;
    }
    .tooltip-line:last-child {
      margin-bottom: 0;
    }
    .tooltip-label {
      color: #aaa;
      margin-right: 10px;
    }
    .time-frame-selector {
      display: flex;
      justify-content: center;
      gap: 10px;
      margin-top: 20px;
    }
    .time-frame-btn {
      background-color: #3d3d5c;
    }
    .time-frame-btn.active {
      background-color: #6c5ce7;
    }
    .instructions {
      margin-top: 20px;
      padding: 15px;
      background-color: #2d2d44;
      border-radius: 5px;
      font-size: 0.9em;
      line-height: 1.5;
    }
  </style>
</head>
<body>
  <div id="app" class="container">
    <div class="header">
      <h1>股票K线图</h1>
      <p>支持缩放、拖动查看，包含成交量和技术指标</p>
    </div>

    <div class="chart-container">
      <div class="chart-header">
        <div class="stock-info">
          <div class="stock-name">示例股票 (AAPL)</div>
          <div class="price" :class="currentPrice >= stockData[0]?.收盘价 ? 'up' : 'down'">
            {{ currentPrice.toFixed(2) }}
          </div>
          <div class="change" :class="priceChange >= 0 ? 'up' : 'down'">
            {{ priceChange >= 0 ? '+' : '' }}{{ priceChange.toFixed(2) }} ({{ priceChangePercent.toFixed(2) }}%)
          </div>
        </div>
        <div class="controls">
          <button @click="zoomIn">放大</button>
          <button @click="zoomOut">缩小</button>
          <button @click="resetZoom">重置</button>
        </div>
      </div>

      <div class="chart-wrapper" ref="chartWrapper">
        <canvas ref="canvas" @mousedown="startDrag" @mousemove="onDrag" @mouseup="endDrag" @mouseleave="endDrag"
                @touchstart="startDrag" @touchmove="onDrag" @touchend="endDrag" @wheel="onWheel"></canvas>
        <div class="tooltip" v-if="tooltip.visible" :style="{ left: tooltip.x + 'px', top: tooltip.y + 'px' }">
          <div class="tooltip-line">
            <span class="tooltip-label">日期:</span>
            <span>{{ tooltip.data.日期 }}</span>
          </div>
          <div class="tooltip-line">
            <span class="tooltip-label">开盘:</span>
            <span>{{ tooltip.data.开盘价.toFixed(2) }}</span>
          </div>
          <div class="tooltip-line">
            <span class="tooltip-label">最高:</span>
            <span>{{ tooltip.data.最高价.toFixed(2) }}</span>
          </div>
          <div class="tooltip-line">
            <span class="tooltip-label">最低:</span>
            <span>{{ tooltip.data.最低价.toFixed(2) }}</span>
          </div>
          <div class="tooltip-line">
            <span class="tooltip-label">收盘:</span>
            <span>{{ tooltip.data.收盘价.toFixed(2) }}</span>
          </div>
          <div class="tooltip-line">
            <span class="tooltip-label">成交量:</span>
            <span>{{ formatVolume(tooltip.data.成交量) }}</span>
          </div>
        </div>
      </div>

      <div class="legend">
        <div class="legend-item">
          <div class="legend-color" style="background-color: #00b894;"></div>
          <span>阳线</span>
        </div>
        <div class="legend-item">
          <div class="legend-color" style="background-color: #ff7675;"></div>
          <span>阴线</span>
        </div>
        <div class="legend-item">
          <div class="legend-color" style="background-color: #74b9ff;"></div>
          <span>5日均线</span>
        </div>
        <div class="legend-item">
          <div class="legend-color" style="background-color: #a29bfe;"></div>
          <span>20日均线</span>
        </div>
        <div class="legend-item">
          <div class="legend-color" style="background-color: #ffeaa7;"></div>
          <span>40日均线</span>
        </div>
      </div>
    </div>

    <div class="time-frame-selector">
      <button class="time-frame-btn" :class="{ active: timeFrame === '1D' }" @click="setTimeFrame('1D')">1日</button>
      <button class="time-frame-btn" :class="{ active: timeFrame === '1W' }" @click="setTimeFrame('1W')">1周</button>
      <button class="time-frame-btn" :class="{ active: timeFrame === '1M' }" @click="setTimeFrame('1M')">1月</button>
      <button class="time-frame-btn" :class="{ active: timeFrame === '3M' }" @click="setTimeFrame('3M')">3月</button>
      <button class="time-frame-btn" :class="{ active: timeFrame === '1Y' }" @click="setTimeFrame('1Y')">1年</button>
    </div>

    <div class="instructions">
      <h3>操作指南:</h3>
      <ul>
        <li>鼠标滚轮或双指捏合: 缩放图表</li>
        <li>鼠标拖动或单指滑动: 平移图表</li>
        <li>点击上方按钮: 切换时间周期</li>
        <li>鼠标悬停在K线上: 查看详细数据</li>
      </ul>
    </div>
  </div>

  <script>
    new Vue({
      el: '#app',
      data() {
        return {
          stockData: [],
          currentPrice: 0,
          priceChange: 0,
          priceChangePercent: 0,
          timeFrame: '1M',
          tooltip: {
            visible: false,
            x: 0,
            y: 0,
            data: {}
          },
          // 图表配置
          config: {
            candleWidth: 10,
            gap: 2,
            padding: {
              top: 20,
              right: 60,
              bottom: 80,
              left: 60
            },
            colors: {
              up: '#00b894',
              down: '#ff7675',
              volume: '#636e72',
              ma5: '#74b9ff',
              ma20: '#a29bfe',
              ma40: '#ffeaa7',
              grid: '#444',
              text: '#e0e0e0'
            }
          },
          // 视图状态
          viewState: {
            offset: 0,
            scale: 1,
            isDragging: false,
            lastX: 0
          },
          canvas: null,
          ctx: null,
          canvasWidth: 0,
          canvasHeight: 0
        }
      },
      mounted() {
        this.initCanvas();
        this.generateSampleData();
        this.drawChart();
        window.addEventListener('resize', this.handleResize);
      },
      beforeDestroy() {
        window.removeEventListener('resize', this.handleResize);
      },
      methods: {
        initCanvas() {
          this.canvas = this.$refs.canvas;
          this.ctx = this.canvas.getContext('2d');
          this.handleResize();
        },
        handleResize() {
          const wrapper = this.$refs.chartWrapper;
          this.canvasWidth = wrapper.clientWidth;
          this.canvasHeight = wrapper.clientHeight;
          this.canvas.width = this.canvasWidth;
          this.canvas.height = this.canvasHeight;
          this.drawChart();
        },
        generateSampleData() {
          // 生成模拟数据
          const startDate = new Date('2020-01-01');
          const data = [];
          let price = 100;
          
          for (let i = 0; i < 500; i++) {
            const date = new Date(startDate);
            date.setDate(date.getDate() + i);
            
            const open = price;
            const change = (Math.random() - 0.5) * 10;
            const close = open + change;
            const high = Math.max(open, close) + Math.random() * 5;
            const low = Math.min(open, close) - Math.random() * 5;
            const volume = Math.floor(Math.random() * 1000000) + 500000;
            
            // 计算移动平均线（简化计算）
            const ma5 = i >= 4 ? data.slice(i-4, i+1).reduce((sum, item) => sum + item.收盘价, 0) / 5 : open;
            const ma20 = i >= 19 ? data.slice(i-19, i+1).reduce((sum, item) => sum + item.收盘价, 0) / 20 : open;
            const ma40 = i >= 39 ? data.slice(i-39, i+1).reduce((sum, item) => sum + item.收盘价, 0) / 40 : open;
            
            data.push({
              日期: date.toISOString().split('T')[0],
              开盘价: open,
              最高价: high,
              最低价: low,
              收盘价: close,
              成交量: volume,
              '5均价': ma5,
              '20均价': ma20,
              '40均价': ma40
            });
            
            price = close;
          }
          
          this.stockData = data;
          this.currentPrice = data[data.length - 1].收盘价;
          this.priceChange = this.currentPrice - data[0].收盘价;
          this.priceChangePercent = (this.priceChange / data[0].收盘价) * 100;
        },
        drawChart() {
          if (!this.ctx || this.stockData.length === 0) return;
          
          // 清除画布
          this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
          
          // 绘制网格和坐标
          this.drawGrid();
          
          // 计算可见数据范围
          const visibleData = this.getVisibleData();
          if (visibleData.length === 0) return;
          
          // 计算价格范围
          const priceRange = this.calculatePriceRange(visibleData);
          
          // 绘制K线
          this.drawCandles(visibleData, priceRange);
          
          // 绘制移动平均线
          this.drawMALines(visibleData, priceRange);
          
          // 绘制成交量
          this.drawVolume(visibleData);
        },
        drawGrid() {
          const { ctx, canvasWidth, canvasHeight, config } = this;
          const { padding, colors } = config;
          
          ctx.strokeStyle = colors.grid;
          ctx.lineWidth = 0.5;
          ctx.fillStyle = colors.text;
          ctx.font = '12px Arial';
          
          // 水平网格线
          const horizontalLines = 5;
          for (let i = 0; i <= horizontalLines; i++) {
            const y = padding.top + (canvasHeight - padding.top - padding.bottom) * (i / horizontalLines);
            ctx.beginPath();
            ctx.moveTo(padding.left, y);
            ctx.lineTo(canvasWidth - padding.right, y);
            ctx.stroke();
            
            // 价格标签
            const price = this.getPriceForY(y);
            ctx.textAlign = 'right';
            ctx.fillText(price.toFixed(2), padding.left - 5, y + 4);
          }
          
          // 垂直网格线（时间轴）
          const visibleData = this.getVisibleData();
          if (visibleData.length > 0) {
            const step = Math.max(1, Math.floor(visibleData.length / 5));
            for (let i = 0; i < visibleData.length; i += step) {
              const x = this.getXForIndex(i);
              ctx.beginPath();
              ctx.moveTo(x, padding.top);
              ctx.lineTo(x, canvasHeight - padding.bottom);
              ctx.stroke();
              
              // 日期标签
              ctx.textAlign = 'center';
              ctx.fillText(visibleData[i].日期, x, canvasHeight - padding.bottom + 20);
            }
          }
          
          // 绘制Y轴标签（右侧）
          ctx.textAlign = 'left';
          for (let i = 0; i <= horizontalLines; i++) {
            const y = padding.top + (canvasHeight - padding.top - padding.bottom) * (i / horizontalLines);
            const price = this.getPriceForY(y);
            ctx.fillText(price.toFixed(2), canvasWidth - padding.right + 5, y + 4);
          }
        },
        drawCandles(visibleData, priceRange) {
          const { ctx, config } = this;
          const { padding, colors } = config;
          const chartHeight = this.canvasHeight - padding.top - padding.bottom;
          
          for (let i = 0; i < visibleData.length; i++) {
            const data = visibleData[i];
            const x = this.getXForIndex(i);
            const isUp = data.收盘价 >= data.开盘价;
            
            // 计算K线的Y坐标和高度
            const highY = this.getYForPrice(data.最高价, priceRange);
            const lowY = this.getYForPrice(data.最低价, priceRange);
            const openY = this.getYForPrice(data.开盘价, priceRange);
            const closeY = this.getYForPrice(data.收盘价, priceRange);
            
            // 绘制上下影线
            ctx.strokeStyle = isUp ? colors.up : colors.down;
            ctx.lineWidth = 1;
            ctx.beginPath();
            ctx.moveTo(x, highY);
            ctx.lineTo(x, lowY);
            ctx.stroke();
            
            // 绘制实体
            const bodyTop = Math.min(openY, closeY);
            const bodyHeight = Math.abs(openY - closeY);
            const bodyWidth = Math.max(1, this.config.candleWidth * this.viewState.scale);
            
            ctx.fillStyle = isUp ? colors.up : colors.down;
            ctx.fillRect(x - bodyWidth/2, bodyTop, bodyWidth, bodyHeight);
          }
        },
        drawMALines(visibleData, priceRange) {
          const { ctx, config } = this;
          const { colors } = config;
          
          // 绘制5日均线
          this.drawMALine(visibleData, priceRange, '5均价', colors.ma5);
          
          // 绘制20日均线
          this.drawMALine(visibleData, priceRange, '20均价', colors.ma20);
          
          // 绘制40日均线
          this.drawMALine(visibleData, priceRange, '40均价', colors.ma40);
        },
        drawMALine(visibleData, priceRange, maKey, color) {
          const { ctx } = this;
          
          ctx.strokeStyle = color;
          ctx.lineWidth = 1.5;
          ctx.beginPath();
          
          for (let i = 0; i < visibleData.length; i++) {
            const data = visibleData[i];
            if (data[maKey] === undefined) continue;
            
            const x = this.getXForIndex(i);
            const y = this.getYForPrice(data[maKey], priceRange);
            
            if (i === 0) {
              ctx.moveTo(x, y);
            } else {
              ctx.lineTo(x, y);
            }
          }
          
          ctx.stroke();
        },
        drawVolume(visibleData) {
          const { ctx, canvasHeight, config } = this;
          const { padding, colors } = config;
          
          const volumeHeight = 100;
          const volumeTop = canvasHeight - padding.bottom - volumeHeight;
          
          // 计算成交量范围
          const volumeRange = this.calculateVolumeRange(visibleData);
          
          // 绘制成交量柱
          for (let i = 0; i < visibleData.length; i++) {
            const data = visibleData[i];
            const x = this.getXForIndex(i);
            const isUp = data.收盘价 >= data.开盘价;
            const volumeY = this.getYForVolume(data.成交量, volumeRange, volumeHeight);
            const volumeWidth = Math.max(1, this.config.candleWidth * this.viewState.scale * 0.8);
            
            ctx.fillStyle = isUp ? colors.up : colors.down;
            ctx.fillRect(x - volumeWidth/2, volumeY, volumeWidth, volumeTop + volumeHeight - volumeY);
          }
          
          // 绘制成交量区域边框
          ctx.strokeStyle = colors.grid;
          ctx.lineWidth = 0.5;
          ctx.strokeRect(padding.left, volumeTop, this.canvasWidth - padding.left - padding.right, volumeHeight);
        },
        drawMALine(visibleData, priceRange, maKey, color) {
          const { ctx } = this;
          
          ctx.strokeStyle = color;
          ctx.lineWidth = 1.5;
          ctx.beginPath();
          
          let started = false;
          for (let i = 0; i < visibleData.length; i++) {
            const data = visibleData[i];
            if (data[maKey] === undefined) continue;
            
            const x = this.getXForIndex(i);
            const y = this.getYForPrice(data[maKey], priceRange);
            
            if (!started) {
              ctx.moveTo(x, y);
              started = true;
            } else {
              ctx.lineTo(x, y);
            }
          }
          
          ctx.stroke();
        },
        getVisibleData() {
          const { offset, scale } = this.viewState;
          const start = Math.max(0, Math.floor(offset));
          const end = Math.min(this.stockData.length, Math.ceil(offset + this.stockData.length / scale));
          return this.stockData.slice(start, end);
        },
        calculatePriceRange(visibleData) {
          let min = Number.MAX_VALUE;
          let max = Number.MIN_VALUE;
          
          for (const data of visibleData) {
            min = Math.min(min, data.最低价);
            max = Math.max(max, data.最高价);
          }
          
          // 添加一些边距
          const margin = (max - min) * 0.05;
          return {
            min: min - margin,
            max: max + margin
          };
        },
        calculateVolumeRange(visibleData) {
          let max = 0;
          
          for (const data of visibleData) {
            max = Math.max(max, data.成交量);
          }
          
          return {
            min: 0,
            max: max
          };
        },
        getXForIndex(index) {
          const { padding } = this.config;
          const visibleData = this.getVisibleData();
          const chartWidth = this.canvasWidth - padding.left - padding.right;
          
          if (visibleData.length <= 1) {
            return padding.left + chartWidth / 2;
          }
          
          const relativeIndex = index - this.viewState.offset;
          const totalVisible = this.stockData.length / this.viewState.scale;
          return padding.left + (relativeIndex / totalVisible) * chartWidth;
        },
        getYForPrice(price, priceRange) {
          const { padding } = this.config;
          const chartHeight = this.canvasHeight - padding.top - padding.bottom;
          return padding.top + chartHeight * (1 - (price - priceRange.min) / (priceRange.max - priceRange.min));
        },
        getYForVolume(volume, volumeRange, volumeHeight) {
          const { padding } = this.config;
          const volumeTop = this.canvasHeight - padding.bottom - volumeHeight;
          return volumeTop + volumeHeight * (1 - volume / volumeRange.max);
        },
        getPriceForY(y) {
          const { padding } = this.config;
          const visibleData = this.getVisibleData();
          const priceRange = this.calculatePriceRange(visibleData);
          const chartHeight = this.canvasHeight - padding.top - padding.bottom;
          return priceRange.max - (y - padding.top) / chartHeight * (priceRange.max - priceRange.min);
        },
        startDrag(event) {
          event.preventDefault();
          this.viewState.isDragging = true;
          this.viewState.lastX = this.getEventX(event);
        },
        onDrag(event) {
          if (!this.viewState.isDragging) return;
          event.preventDefault();
          
          const currentX = this.getEventX(event);
          const deltaX = currentX - this.viewState.lastX;
          
          // 计算移动的索引数量
          const visibleCount = this.stockData.length / this.viewState.scale;
          const chartWidth = this.canvasWidth - this.config.padding.left - this.config.padding.right;
          const deltaIndex = (deltaX / chartWidth) * visibleCount;
          
          this.viewState.offset = Math.max(0, Math.min(this.stockData.length - visibleCount, this.viewState.offset - deltaIndex));
          this.viewState.lastX = currentX;
          
          this.drawChart();
          this.updateTooltip(event);
        },
        endDrag() {
          this.viewState.isDragging = false;
        },
        onWheel(event) {
          event.preventDefault();
          
          const delta = Math.sign(event.deltaY);
          const zoomFactor = delta > 0 ? 1.1 : 0.9;
          
          // 计算缩放中心
          const mouseX = this.getEventX(event);
          const visibleCount = this.stockData.length / this.viewState.scale;
          const chartWidth = this.canvasWidth - this.config.padding.left - this.config.padding.right;
          const mouseIndex = (mouseX - this.config.padding.left) / chartWidth * visibleCount + this.viewState.offset;
          
          // 应用缩放
          const newScale = Math.max(1, Math.min(20, this.viewState.scale * zoomFactor));
          
          // 调整偏移量以保持鼠标位置
          const newVisibleCount = this.stockData.length / newScale;
          const newOffset = Math.max(0, Math.min(this.stockData.length - newVisibleCount, 
            mouseIndex - (mouseX - this.config.padding.left) / chartWidth * newVisibleCount));
          
          this.viewState.scale = newScale;
          this.viewState.offset = newOffset;
          
          this.drawChart();
        },
        getEventX(event) {
          if (event.type.includes('touch')) {
            return event.touches[0].clientX - this.canvas.getBoundingClientRect().left;
          } else {
            return event.offsetX;
          }
        },
        updateTooltip(event) {
          const x = this.getEventX(event);
          const visibleData = this.getVisibleData();
          
          if (visibleData.length === 0) {
            this.tooltip.visible = false;
            return;
          }
          
          // 找到最接近的K线
          const chartWidth = this.canvasWidth - this.config.padding.left - this.config.padding.right;
          const index = Math.floor((x - this.config.padding.left) / chartWidth * visibleData.length);
          
          if (index >= 0 && index < visibleData.length) {
            const data = visibleData[index];
            this.tooltip.visible = true;
            this.tooltip.x = x + 10;
            this.tooltip.y = 10;
            this.tooltip.data = data;
          } else {
            this.tooltip.visible = false;
          }
        },
        zoomIn() {
          this.viewState.scale = Math.min(20, this.viewState.scale * 1.2);
          this.drawChart();
        },
        zoomOut() {
          this.viewState.scale = Math.max(1, this.viewState.scale / 1.2);
          this.drawChart();
        },
        resetZoom() {
          this.viewState.scale = 1;
          this.viewState.offset = 0;
          this.drawChart();
        },
        setTimeFrame(timeFrame) {
          this.timeFrame = timeFrame;
          // 在实际应用中，这里应该根据选择的时间范围加载相应的数据
          // 这里我们只是重新生成一些数据来模拟
          this.generateSampleData();
          this.resetZoom();
        },
        formatVolume(volume) {
          if (volume >= 1000000) {
            return (volume / 1000000).toFixed(2) + 'M';
          } else if (volume >= 1000) {
            return (volume / 1000).toFixed(2) + 'K';
          }
          return volume.toString();
        }
      }
    });
  </script>
</body>
</html>